全网最详细的ReentrantReadWriteLock源码剖析(万字长文)

您所在的位置:网站首页 sync 锁升级 全网最详细的ReentrantReadWriteLock源码剖析(万字长文)

全网最详细的ReentrantReadWriteLock源码剖析(万字长文)

2024-07-12 12:17| 来源: 网络整理| 查看: 265

碎碎念) 花了两天时间,终于把ReentrantReadWriteLock(读写锁)解析做完了。之前钻研过AQS(AbstractQueuedSynchronizer)的源码,发现弄懂读写锁也没有想象中那么困难。而且阅读完ReentrantReadWriteLock的源码,正好可以和AQS的源码串起来理解,相辅相成 AQS的链接贴在下方👇👇👇 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(二)资源的获取和释放 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(三)条件变量

简介

ReentrantReadWriteLock是一个可重入读写锁,内部提供了读锁和写锁的单独实现。其中读锁用于只读操作,可被多个线程共享;写锁用于写操作,只能互斥访问

ReentrantReadWriteLock尤其适合读多写少的应用场景

读多写少: 在一些业务场景中,大部分只是读数据,写数据很少,如果这种场景下依然使用独占锁(如synchronized),会大大降低性能。因为独占锁会使得本该并行执行的读操作,变成了串行执行

ReentrantReadWriteLock实现了ReadWriteLock接口,该接口只有两个方法,分别用于返回读锁和写锁,这两个锁都是Lock对象。该接口源码如下:

public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }

ReentrantReadWriteLock有两个域,分别存放读锁和写锁:

private final ReentrantReadWriteLock.ReadLock readerLock; private final ReentrantReadWriteLock.WriteLock writerLock;

ReentrantReadWriteLock的核心原理主要在于两点:

内部类Sync:实现了的AQS大部分方法。Sync类有两个子类FairSync和NonfairSync,分别实现了公平读写锁和非公平读写锁。Sync类及其子类的源码解析会在后面给出 内部类ReadLock和WriteLock:分别是读锁和写锁的具体实现,它们都和ReentrantLock一样实现了Lock接口,因此实现的手段也和ReentrantLock一样,都是委托给内部的Sync类对象来实现,对应的源码解析也会在后面给出

说什么Sync类、ReadLock、WriteLock类啥的都太抽象,不如一张图来得实在!ReentrantReadWriteLock和这些内部类的继承、聚合关系如下图所示:

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15655865.html 版权:本文版权归作者和博客园共有 转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任 ReentrantReadWriteLock的特点 读写锁的互斥关系 读锁和写锁之间是互斥关系:当有线程持有读锁时,写锁不能获得;当有其他线程持有写锁时,读锁不能获得 读锁和读锁之间是共享关系 写锁和写锁之间是互斥关系 可重入性

ReentrantReadWriteLock在ReadWriteLock接口之上,添加了可重入的特性,且读锁和写锁都支持可重入。可重入的含义是:

如果一个线程获取了读锁,那么它可以再次获取读锁(但直接获取写锁会失败,原因见下方的“锁的升降级”) 如果一个线程获取了写锁,那么它可以再次获取写锁或读锁 锁的升降级 锁升级

ReentrantReadWriteLock不支持锁升级,即同一个线程获取读锁后,直接申请写锁是不能获取成功的。测试代码如下:

public class Test1 { public static void main(String[] args) { ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock(); rtLock.readLock().lock(); System.out.println("get readLock."); rtLock.writeLock().lock(); System.out.println("blocking"); } }

运行到第6行会因为获取失败而被阻塞,导致Test1发生死锁。命令行输出如下:

get readLock. 锁降级

ReentrantReadWriteLock支持锁降级,即同一个线程获取写锁后,直接申请读锁是可以直接成功的。测试代码如下:

public class Test2 { public static void main(String[] args) { ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock(); rtLock.writeLock().lock(); System.out.println("writeLock"); rtLock.readLock().lock(); System.out.println("get read lock"); } }

该程序不会产生死锁。结果输出如下:

writeLock get read lock Process finished with exit code 0 读写锁的升降级规则总结 ReentrantReadWriteLock不支持锁升级,因为可能有其他线程同时持有读锁,而读写锁之间是互斥的,因此升级为写锁存在冲突 ReentrantReadWriteLock支持锁降级,因为如果该线程持有写锁时,一定没有其他线程持有读锁或写锁,因此降级为读锁不存在冲突 公平锁和非公平锁

ReentrantReadWriteLock支持公平模式和非公平模式获取锁。从性能上来看,非公平模式更好

二者的规则如下:

公平锁:无论是读线程还是写线程,在申请锁时都会检查是否有其他线程在同步队列中等待。如果有,则让步 非公平锁:如果是读线程,在申请锁时会判断是否有写线程在同步队列中等待。如果有,则让步。不过这是为了防止写线程饿死,与公平策略无关;如果是写线程,则直接竞争锁资源,不会关心有无别的线程正在等待 作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15655865.html 版权:本文版权归作者和博客园共有 转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任 Sync类

Sync类是一个抽象类,有两个具体子类NonfairSync和FairSync,分别对应非公平读写锁、公平读写锁。Sync类的主要作用就是为这两个子类提供绝绝绝大部分的方法实现 只定义了两个抽象方法writerShouldBlock和readerShouldBlocker交给两个子类去实现

读状态和写状态

Sync类利用AQS单个state字段,来同时表示读状态和写状态,源码如下:

abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 6317671515068378041L; /* * Read vs write count extraction constants and functions. * Lock state is logically divided into two unsigned shorts: * The lower one representing the exclusive (writer) lock hold count, * and the upper the shared (reader) hold count. */ static final int SHARED_SHIFT = 16; static final int SHARED_UNIT = (1 SHARED_SHIFT; } /** Returns the number of exclusive holds represented in count */ static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } // ······ };

根据上面源码可以看出:

SHARED_SHIFT表示AQS中的state(int型,32位)的高16位,作为读状态,低16位作为写状态 SHARED_UNIT二级制为2^16,读锁加1,state加SHARED_UNIT MAX_COUNT就是写或读资源的最大数量,为2^16-1 使用sharedCount方法获取读状态,使用exclusiveCount方法获取获取写状态

state划分为读、写状态的示意图(图来自网络)如下,其中读锁持有1个,写锁持有3个:

记录首个获得读锁的线程 private transient Thread firstReader = null; private transient int firstReaderHoldCount;

firstReader记录首个获得读锁的线程;firstReaderHoldCount记录firstReader持有的读锁数

线程局部计数器

Sync类定义了一个线程局部变量readHolds,用于保存当前线程重入读锁的次数。如果该线程的读锁数减为0,则将该变量从线程局部域中移除。相关源码如下:

// 内部类,用于记录当前线程重入读锁的次数 static final class HoldCounter { int count = 0; // 这里使用线程的id而非直接引用,是为了方便GC final long tid = getThreadId(Thread.currentThread()); } /* 内部类,继承ThreadLocal,该类型的变量是每个线程各自保存一份,其中保存的是HoldCounter对象,用set方法保存,get方法获取 */ static final class ThreadLocalHoldCounter extends ThreadLocal { public HoldCounter initialValue() { return new HoldCounter(); } } private transient ThreadLocalHoldCounter readHolds;

由于readHolds变量是线程局部变量(继承ThreadLocal类),每个线程都会保存一份副本,不同线程调用其get方法返回的HoldCounter对象不同

readHolds中的HoldCounter变量保存了每个读线程的重入次数,即其持有的读锁数量。这么做的目的是便于线程释放读锁时进行合法性判断:线程在不持有读锁的情况下释放锁是不合法的,需要抛出IllegalMonitorStateException异常

缓存

Sync类定义了一个HoldCounter变量cachedHoldCounter,用于保存最近获取到读锁的线程的重入次数。源码如下:

// 这是一个启发式算法 private transient HoldCounter cachedHoldCounter;

设计该变量的目的是:将其作为一个缓存,加快代码执行速度。因为获取、释放读锁的线程往往都是最近获取读锁的那个线程,虽然每个线程的重入次数都会使用readHolds来保存,但使用readHolds变量会涉及到ThreadLocal内部的查找(lookup),这是存在一定开销的。有了cachedHoldCounter这个缓存后,就不用每次都在ThreadLocal内部查找,加快了代码执行速度。相当于用空间换时间

获取锁

无论是公平锁还是非公平锁,它们获取锁的逻辑都是相同的,因此Sync类在这一层就提供了统一的实现

但是,获取写锁和获取读锁的逻辑不相同:

写锁是互斥资源,获取写锁的逻辑主要在tryAcquire方法 读锁是共享资源,获取读锁的逻辑主要在tryAcquireShared方法

具体的源码分析见下方的“读锁”和“写锁”各自章节的“获取x锁”部分

释放锁

无论是公平锁还是非公平锁,它们释放锁的逻辑都是相同的,因此Sync类在这一层就提供了统一的实现

但是,释放写锁和释放读锁的逻辑不相同:

写锁是互斥资源,释放写锁的逻辑主要在tryRelease方法 读锁是共享资源,释放读锁的逻辑主要在tryReleaseShared方法

具体的源码分析见下方的“读锁”和“写锁”各自章节的“释放x锁”部分

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15655865.html 版权:本文版权归作者和博客园共有 转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任 写锁

写锁是由内部类WriteLock实现的,其实现了Lock接口,获取锁、释放锁的逻辑都委托给了sync域(Sync对象)来执行。WriteLock的基本结构如下:

public static class WriteLock implements Lock, java.io.Serializable { private static final long serialVersionUID = -4992448646407690164L; private final Sync sync; // 构造方法注入Sync类对象 protected WriteLock(ReentrantReadWriteLock lock) { sync = lock.sync; } // 实现了Lock接口的所有方法 } 获取写锁

WriteLock使用lock方法获取写锁,一次获取一个写锁,源码如下:

public void lock() { sync.acquire(1); }

lock方法内部实际调用的是AQS的acquire方法,源码如下:

public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }

而acquire方法会调用子类Sync实现的tryAcquire方法,如下:

protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); if (c != 0) { if (w == 0 || current != getExclusiveOwnerThread()) return false; // 如果是读锁被获取中,或写锁被获取但不是本线程获取的,则获取失败 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire setState(c + acquires); return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; // 如果根据公平性判断此时写线程需要被阻塞,或在获取过程中发生竞争且竞争失败,则获取失败 setExclusiveOwnerThread(current); return true; }

分为三步: 1、如果读锁正在被获取中,或者写锁被获取中但不是本线程持有,则获取失败 2、如果获取写锁达到饱和,则抛出错误 3、如果上面两个都不成立,说明此线程可以请求写锁。但需要先根据公平策略来判断是否应该先阻塞。如果不用阻塞,且CAS成功,则获取成功。否则获取失败

其中公平策略判断所调用的writerShouldBlock,在后面分析公平锁和非公平锁时会给出分析

如果tryAcquire方法获取写锁成功,则acquire方法直接返回,否则进入同步队列阻塞等待

tryAcquire体现的读写锁的特征:

互斥关系: 写锁和写锁之间是互斥的:如果是别的线程持有写锁,那么直接返回false 读锁和写锁之间是互斥的。当有线程持有读锁时,写锁不能获得:如果c!=0且w==0,说明此时有线程持有读锁,直接返回false 可重入性:如果当前线程持有写锁,就不用进行公平性判断writerShouldBlock,请求锁一定会获取成功 不允许锁升级:如果当前线程持有读锁,想要直接申请写锁,此时c!=0且w==0,而exclusiveOwnerThread是null,不等于current,直接返回false 释放写锁

WriteLock使用unlock方法释放写锁,如下:

public void unlock() { sync.release(1); }

unlock内部实际上调用的是AQS的release方法,源码如下:

public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

而该方法会调用子类Sync实现的tryAcquire方法,源码如下:

protected final boolean tryRelease(int releases) { // 如果并不持有锁就释放,会抛出异常 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); // 如果释放锁之后锁空闲,那么需要将锁持有者置为null int nextc = getState() - releases; boolean free = exclusiveCount(nextc) == 0; if (free) setExclusiveOwnerThread(null); setState(nextc); return free; // 返回锁释放后是否空闲 }

注意: 任何锁的释放都需要判断是否是在持有锁的情况下。如果不持有锁就释放,会抛出异常。对于写锁来说,判断是否持有锁很简单,只需要调用isHeldExclusively方法进行判断即可;而对于读锁来说,判断是否持有锁比较复杂,需要根据每个线程各自保存的持有读锁数来判断,即readHolds中保存的变量

尝试获取写锁

WriteLock使用tryLock来尝试获取写锁,如下:

public boolean tryLock( ) { return sync.tryWriteLock(); }

tryLock内部实际调用的是Sync类定义并实现的tryWriteLock方法。该方法是一个final方法,不允许子类重写。其源码如下:

final boolean tryWriteLock() { Thread current = Thread.currentThread(); int c = getState(); if (c != 0) { int w = exclusiveCount(c); if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w == MAX_COUNT) throw new Error("Maximum lock count exceeded"); } if (!compareAndSetState(c, c + 1)) // 相比于tryAcquire方法,这里缺少对公平性判断(writerShouldBlock) return false; setExclusiveOwnerThread(current); return true; }

其实除了缺少对公平策略判断writerShouldBlock的调用以外,和tryAcquire方法基本上是一样的,这里不再废话

Lock接口其他方法的实现 // 支持中断响应的lock方法,实际上调用的是AQS的acquireInterruptibly方法 public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } // 实际上调用的是AQS的方法tryAcquireNanos方法 public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } // 实际上调用的是Sync类实现的newCondition方法 public Condition newCondition() { return sync.newCondition(); }

写锁支持创建条件变量,因为写锁是独占锁,而条件变量在await时会释放掉所有锁资源。写锁能够保证所有的锁资源都是本线程所持有,所以可以放心地去释放所有的锁

而读锁不支持创建条件变量,因为读锁是共享锁,可能会有其他线程持有读锁。如果调用await,不仅会释放掉本线程持有的读锁,也会释放掉其他线程持有的读锁,这是不被允许的。因此读锁不支持条件变量

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15655865.html 版权:本文版权归作者和博客园共有 转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任 读锁

读锁是由内部类ReadLock实现的,其实现了Lock接口,获取锁、释放锁的逻辑都委托给了Sync类实例sync来执行。ReadLock的基本结构如下:

public static class ReadLock implements Lock, java.io.Serializable { private static final long serialVersionUID = -5992448646407690164L; private final Sync sync; // 构造方法注入Sync类对象 protected ReadLock(ReentrantReadWriteLock lock) { sync = lock.sync; } // 实现了Lock接口的所有方法 } 获取读锁

ReadLock使用lock方法获取读锁,一次获取一个读锁。源码如下:

public void lock() { sync.acquireShared(1); }

lock方法内部实际调用的是AQS的acquireShared方法,源码如下:

public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }

该方法会调用Sync类实现的tryAcquireShared方法,源码如下:

protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; // 如果写锁被获取,且并不是由本线程持有写锁,那么获取失败 int r = sharedCount(c); if (!readerShouldBlock() && // 先进行公平性判断是否应该让步,这可能会导致重入读锁的线程获取失败 r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { // CAS失败可能也会导致本能获取成功的线程获取失败 // 如果此时读锁没有被获取,则该线程是第一个获取读锁的线程,记录相应信息 if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } // 该线程不是首个获取读锁的线程,需要记录到readHolds中 else { HoldCounter rh = cachedHoldCounter; // 通常当前获取读锁的线程就是最近获取到读锁的线程,所以直接用缓存 // 还是需要判断一下是不是最近获取到读锁的线程。如果不是,则调用get创建一个新的局部HoldCounter变量 if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); // 之前最近获取读锁的线程如果释放完了读锁而导致其局部HoldCounter变量被remove了,这里重新获取就重新set else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; // 如果公平性判断无需让步,且读锁数未饱和,且CAS竞争成功,则说明获取成功 } return fullTryAcquireShared(current); }

tryAcquireShared的返回值说明:

负数:获取失败,线程会进入同步队列阻塞等待 0:获取成功,但是后续以共享模式获取的线程都不可能获取成功(这里暂时用不上) 正数:获取成功,且后续以共享模式获取的线程也可能获取成功

在读写锁中,tryAcquireShared没有返回0的情况,只会返回正数或负数

前面“Sync类”中讲解过这些变量,这里再复习一遍:

firstReader、firstReaderHoldCount分别用于记录第一个获取到写锁的线程及其持有读锁的数量 cachedHoldCounter用于记录最近获取到写锁的线程持有读锁的数量 readHolds是一个线程局部变量(ThreadLocal变量),用于保存每个获得读锁的线程各自持有的读锁数量

tryAcquireShared的流程如下: 1、如果其他线程持有写锁,那么获取失败(返回-1) 2、否则,根据公平策略判断是否应该阻塞。如果不用阻塞且读锁数量未饱和,则CAS请求读锁。如果CAS成功,获取成功(返回1),并记录相关信息 3、如果根据公平策略判断应该阻塞,或者读锁数量饱和,或者CAS竞争失败,那么交给完整版本的获取方法fullTryAcquireShared去处理

其中上述步骤2如果发生了重入读(当前线程持有读锁的情况下,再次请求读锁),但根据公平策略判断该线程需要阻塞等待,而导致重入读失败。按照正常逻辑,重入读不应该失败。不过,tryAcquireShared并没有处理这种情况,而是将其放到了fullTryAcquireShared中进行处理。此外,CAS竞争失败而导致获取读锁失败,也交给fullTryAcquireShared去处理(fullTryAcquireShared表示我好难-_-)

fullTryAcquireShared方法是尝试获取读锁的完全版本,用于处理tryAcquireShared方法未处理的: 1、CAS竞争失败 2、因公平策略判断应该阻塞而导致的重入读失败

这两种情况。其源码如下:

final int fullTryAcquireShared(Thread current) { HoldCounter rh = null; for (;;) { int c = getState(); if (exclusiveCount(c) != 0) { if (getExclusiveOwnerThread() != current) return -1; // else we hold the exclusive lock; blocking here // would cause deadlock. } else if (readerShouldBlock()) { // 如果当前线程就是firstReader,那么它一定是重入读,不让它失败,而是重新loop直到公平性判断不阻塞为止 if (firstReader == current) { // assert firstReaderHoldCount > 0; } else { if (rh == null) { rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) { rh = readHolds.get(); if (rh.count == 0) readHolds.remove(); } } if (rh.count == 0) // 当前线程既不持有读锁(不是重入读),并且被公平性判断为应该阻塞,那么就获取失败 return -1; } } if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); // 下面的逻辑基本上和tryAcquire中差不多,不过这里的CAS如果失败,会重新loop直到成功为止 if (compareAndSetState(c, c + SHARED_UNIT)) { if (sharedCount(c) == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { if (rh == null) rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; cachedHoldCounter = rh; // cache for release } return 1; } } }

fullTryAcquireShared其实和tryAcquire存在很多的冗余之处,但这么做的目的主要是让tryAcquireShared变得更简单,不用处理复杂的CAS循环

fullTryAcquireShared主要是为了处理CAS失败和readerShouldBlock判true而导致的重入读失败,这两种情况在理论上都应该成功获取锁。fullTryAcquireShared的做法就是将这两种情况放在for循环中,一旦发生就重新循环,直到成功为止

tryAcquireShared和fullTryAcquireShared体现的读写锁特征:

互斥关系: 读锁和读锁之间是共享的:即使有其他线程持有了读锁,当前线程也能获取读锁 读锁和写锁之间是互斥的。当有其他线程持有写锁,读锁不能获得:tryAcquireShared第4-6行,fullTryAcquireShared第5-7行都能体现这一特征 可重入性:如果当前线程获取了读锁,那么它再次申请读锁一定能成功。这部分逻辑是由fullTryAcquireShared的for循环实现的 支持锁降级:如果当前线程持有写锁,那么它申请读锁一定会成功。这部分逻辑见tryAcquireShared第5行,current和exclusiveOwnerThread是相等的,不会返回-1 释放读锁

ReadLock使用unlock方法释放读锁,如下:

public void unlock() { sync.releaseShared(1); }

unlock方法实际调用的是AQS的releaseShared方法,如下:

public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }

而该方法会调用Sync类实现的tryReleaseShared方法,源码如下:

protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); if (firstReader == current) { if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; // 一般释放锁的都是最后获取锁的那个线程 if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3